package com.frp.lib.client;

import com.frp.lib.config.ClientCommon;
import com.frp.lib.config.ClientProperties;
import com.frp.lib.config.ProxyModel;
import com.frp.lib.handler.*;
import com.frp.lib.message.Message;
import com.frp.lib.utils.Crypto;
import com.frp.lib.utils.NettyUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Created by hujianjun on 11/23 0023.
 */
public class ClientControl implements ClientApi, Runnable {
    private static Logger logger = LoggerFactory.getLogger(ClientControl.class);
    private ClientCommon common;
    private Map<String, ProxyModel.ProxyDefine> proxyDefineMap;
    private Map<String, Proxy> proxies = new HashMap<String, Proxy>();
    private String runId = "";   //runner id after login
    private boolean exit = false;   //if will exit
    private Date pongLast = new Date();
    private Channel channel;        //control connection channel
    private ScheduledExecutorService scheduled = Executors.newSingleThreadScheduledExecutor();

    public void setProperties(ClientProperties properties) {
        this.common = properties.getCommon();
        this.proxyDefineMap = properties.getProxy();
        for(String name : this.proxyDefineMap.keySet()) {
            ProxyModel.ProxyDefine define = this.proxyDefineMap.get(name);
            define.setName(name);
        }
    }

    public Proxy getProxy(String name) {
        return this.proxies.get(name);
    }

    public void addProxy(String name, Proxy proxy) {
        this.proxies.put(name, proxy);
    }

    public String getRunId() {
        return this.runId;
    }

    public ProxyModel.ProxyDefine getProxyDefine(String name) {
        return this.proxyDefineMap.get(name);
    }

    public void init() {
        logger.info("properties common2:{}", this.common);
        logger.info("properties proxy2:{}", this.proxyDefineMap);
        Executors.newSingleThreadScheduledExecutor().schedule(this, 1, TimeUnit.SECONDS);
        scheduled.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                ClientControl.this.checkConnect();
            }
        }, 5, 5, TimeUnit.SECONDS);

        scheduled.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                ClientControl.this.checkHeartBeat();
            }
        }, 5, 30, TimeUnit.SECONDS);

        scheduled.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                ClientControl.this.checkPongTimeout();
            }
        }, 5, 5, TimeUnit.SECONDS);
    }

    // 1. login
    // 2. start reader() writer() manager()
    // 3. connection closed
    // 4. In reader(): close closedCh and exit, controler() get it
    // 5. In controler(): close readCh and sendCh, manager() and writer() will exit
    // 6. In controler(): ini readCh, sendCh, closedCh
    // 7. In controler(): start new reader(), writer(), manager()
    // controler() will keep running
    public void run() {
        this.connect();
    }

    public void newWorkConn() {

    }

    public boolean close() {
        if (this.channel!=null && this.channel.isOpen())
            this.channel.close();
        return true;
    }

    public boolean login() {
        Message.Login login = new Message.Login();
        login.setVersion("0.13.0");
        login.setHostname("");
        login.setOs("windows");
        login.setArch("amd64");
        login.setUser("test");
        long now = new Date().getTime()/1000;
//        long now = 1511942055;
        String auth = this.common.getPrivilegeToken() + now;
        String code = Crypto.md5(auth);

        login.setPrivilege_key(code);
        login.setTimestamp(now);
        login.setRun_id("");
        login.setPool_count(1);

        this.channel.writeAndFlush(login);

        return true;
    }

    public Object connectServer() {
        return 0;
    }

    public void manager() {

    }

    public void controller() {

    }

    private void checkConnect() {
        if (channel!=null && channel.isOpen()==true) {
            return;
        }
        if(exit) {
            //todo will exit
            return;
        }
        logger.info("will reconnect to server.");
        pongLast = new Date();
        this.connect();     //re connect
    }

    private boolean isOnline() {
        if (null == this.channel)
            return false;
        if (!this.channel.isOpen())
            return false;
        if (StringUtil.isNullOrEmpty(this.runId))
            return false;
        return true;
    }

    private void checkHeartBeat() {
        if (!isOnline())
            return;
        Message.Ping ping = new Message.Ping();
        this.channel.writeAndFlush(ping);
//        logger.info("send ping.");
    }

    private void checkPongTimeout() {
        if (!isOnline())
            return;
        Date now = new Date();
        if(now.getTime() - pongLast.getTime() - this.common.getHeadBeatTimeout() * 1000 > 0) {
            logger.warn("pong timeout, will close connection.");
            this.close();
        }
    }

    private void checkProxy() {
        if (!isOnline())
            return;
        for (String name : this.proxyDefineMap.keySet()) {
            ProxyModel.ProxyDefine define = this.proxyDefineMap.get(name);
            if (null == this.proxies.get(name)) {
//                Proxy proxy = Proxy.BaseProxy.create(this, define);
                logger.info("try to register proxy {}", name);
                Message.NewProxy newProxy = ProxyModel.packToMsg(define);
                this.channel.writeAndFlush(newProxy);
            }
        }
    }

    public void connect() {
        if (null!=channel) {
            try {
                channel.close().sync();
            } catch (InterruptedException e) {
                logger.error("close ctx error.", e);
            }
            channel = null;
        }
        ClientHandler handler = new ClientHandler(this.common.getPrivilegeToken(), this);
        try {
            this.channel = NettyUtils.connect(this.common.getServerAddr(), this.common.getServerPort());
        } catch (Exception e) {
            logger.error("connect error", e);
            return;
        }
        ChannelPipeline p = this.channel.pipeline();
//        p.addLast("log", new LoggingHandler("log", LogLevel.INFO));
        p.addLast("msg decoder", new MessageDecoder());
        p.addLast("msg encoder", new MessageEncoder());
        p.addLast("client", handler);

        int timeWait = 0;
        while (!channel.isActive()) {
            try {
                Thread.sleep(100);
                timeWait += 100;
                if (timeWait>=3000) {
                    //最多等待3秒
                    break;
                }
            } catch (InterruptedException e) {
            }
        }

        if (channel.isActive()) {
            logger.info("control connection created success :{}", channel);
        } else {
            logger.error("can not connect to {}:{}", this.common.getServerAddr(), this.common.getServerPort());
            this.close();
            return;
        }
        this.login();
    }

    public void onLoginResp(ChannelHandlerContext ctx, Message.LoginResp loginResp) {
        if (!StringUtil.isNullOrEmpty(loginResp.getError())) {
            logger.error(loginResp.getError());
            return;
        }

        this.runId = loginResp.getRun_id();
        logger = LoggerFactory.getLogger(this.runId);
        logger.info("login to server success, get run id {}", this.runId);
        this.pongLast = new Date();

        ChannelPipeline p = ctx.pipeline();
        p.addLast("aes_encoder", new AESCFBEncoder(this.common.getPrivilegeToken()));
        p.addLast("aes_decoder", new AESCFBDecoder(this.common.getPrivilegeToken()));
//        p.addLast("log1", new LoggingHandler("log1", LogLevel.INFO));
        p.addLast("msg decoder1", new MessageDecoder());
        p.addLast("msg encoder1", new MessageEncoder());
        p.addLast("client1", new ClientHandler(this.common.getPrivilegeToken(), this));
//        p.remove("log");
        p.remove("msg encoder");
        p.remove("msg decoder");
        p.remove("client");

        this.checkProxy();
    }

    public void onMessage(Message msg) {
        if (msg instanceof Message.Pong) {
            this.onPong((Message.Pong)msg);
        } else if (msg instanceof Message.NewProxyResp) {
            this.onNewProxyResp((Message.NewProxyResp) msg);
        } else if (msg instanceof Message.ReqWorkConn) {
            this.onReqWorkConn((Message.ReqWorkConn) msg);
        }
    }

    public void onPong(Message.Pong pong) {
        this.pongLast = new Date();
        logger.debug("receive pong.");
    }

    private void onNewProxyResp(Message.NewProxyResp msg) {
        if (!StringUtil.isNullOrEmpty(msg.getError())) {
            logger.warn("{} start error: {}", msg.getProxy_name(), msg.getError());
            return;
        }

        ProxyModel.ProxyDefine proxyDefine = this.proxyDefineMap.get(msg.getProxy_name());
        if (null == proxyDefine) {
            logger.warn("{} no proxy conf found.", msg.getProxy_name());
            return;
        }
        Proxy proxy = this.proxies.get(msg.getProxy_name());
        if (null != proxy) {
            //close old proxy
            proxy.close();
        }

        proxy = Proxy.BaseProxy.create(this, proxyDefine);
        proxy.run();

        this.proxies.put(proxyDefine.getName(), proxy);
        logger.info("star proxy [{}] success, remote port:{}", proxyDefine.getName(), proxyDefine.getRemote_port());
    }

    private void onReqWorkConn(Message.ReqWorkConn msg) {
        ProxyHandler handler = new ProxyHandler(this.common.getPrivilegeToken(), this);
        Channel channel = NettyUtils.connectWork(this.common.getServerAddr(), this.common.getServerPort());
        ChannelPipeline p = channel.pipeline();
//        p.addLast("log", new LoggingHandler("proxy", LogLevel.INFO));
        p.addLast("msg decoder", new MessageDecoder());
        p.addLast("msg encoder", new MessageEncoder());
        p.addLast("proxy", handler);
        logger.info("connect work success :{}", channel);
        Message.NewWorkConn newWorkConn = new Message.NewWorkConn();
        newWorkConn.setRun_id(runId);
        channel.writeAndFlush(newWorkConn);
    }

    public void onStartWorkConn(ChannelHandlerContext ctx, Message.StartWorkConn msg) {
        String name = msg.getProxy_name();
        Proxy proxy = this.getProxy(name);
        if (null == proxy) {
            throw new RuntimeException("no proxy name:" + name);
        }
        logger.info("start work conn:{}, {}", msg.getProxy_name(), ctx.channel().localAddress());

//        proxy.inWorkConn(ctx.channel());
        ChannelPipeline p = ctx.pipeline();
        //prepare pipeline to forward
//        p.addLast("log2", new LoggingHandler("proxy-" + name+"-"+ctx.channel().localAddress(), LogLevel.INFO));
        ForwardHandler handler = new ForwardHandler(proxy);
        handler.initLocal(ctx);
        p.addLast("forward", handler);
        //clear old pipeline
//        p.remove("log");
        p.remove("msg decoder");
        p.remove("msg encoder");
        p.remove("proxy");
    }

}
